package udpInvoker.main.lib;


import udpInvoker.exceptions.ClassNotTargetForInvocationException;
import udpInvoker.util.UdpUtils;
import udpInvoker.main.*;

import java.lang.reflect.*;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.ArrayList;


/**
 * This class should be extended by classes containing methods one would like to make remotely invokable. No annotation is needed, all public methods within the class will automatically be made available.
 * At least one instance of the class (the class extending Invokable) must be created in order to invoke it's methods remotely. Each instance of the class(es) extending Invokable will contain a separate thread.
 * Copyright 2008  Espen Skjervold, FFI
 */
public abstract class Invokable implements Runnable, MessageSubscriber {


    private String password;
    private boolean encryptResponse;
    private boolean stop;
    private BlockingQueue messagesQueue;

    private int transportType=1;
    private String unicastAddress;
    private int port;

    public static final int MULTICAST=1;
    public static final int UNICAST=2;

    public Invokable() {



        init();


        int i=0;

    }




    public Invokable(String password) {

        this.password  = password;
        init();

    }


    private void init() {

        messagesQueue = new LinkedBlockingQueue();
        

        UdpMulticastReceiver udpMulticastReceiver = UdpMulticastReceiver.getInstance();
        udpMulticastReceiver.subscribe(this);


        
        startUdpListener();
    }



    private void startUdpListener() {

        Thread listenerThread = new Thread(this);

        listenerThread.start();


    }

//    lib void setUnicastAddress(String address, int port) {
//        unicastAddress=address;
//        this.port = port;
//
//        if (address!=null) {
//            transportType=UNICAST;
//            UdpUnicastReceiver udpUnicastReceiver = UdpUnicastReceiver.getInstance(unicastAddress, port);
//            udpUnicastReceiver.subscribe(this);
//        }
//        else
//            transportType=MULTICAST;
//
//    }

    public void setPassword(String password) {
        this.password = password;
    }


    public void run() {


          

            while (true) {

                
                 try {

                    byte[] buffer = (byte[])messagesQueue.take();

                    if (stop)
                        return; //exit listener

                    InvocationMessage incomingMessage = UdpUtils.deSerializeInvocationMessage(buffer, password); //UdpUtils.receiveMessage(multicastSocket, password);




                    if (incomingMessage!=null && incomingMessage.getType()== InvocationMessage.INVOCATION) {
                        System.out.println("Invokable reveived invocation-message");
                        processInvocationMessage(incomingMessage);
                    }
                    else if (incomingMessage!=null && incomingMessage.getType()==InvocationMessage.PING) {
                        System.out.println("Invokable reveived ping-message");
                        processPingMessage(incomingMessage);
                    }
                    else if (incomingMessage!=null && incomingMessage.getType()== InvocationMessage.SERVICE_REQUEST) {
                        System.out.println("Invokable reveived service request");
                        processServiceRequest(incomingMessage);
                    }


              } catch (Exception e) {
                //e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
              }

            }







    }

    private void processServiceRequest(InvocationMessage serviceRequest) {


       
            Class c = this.getClass();

            Method[] methods = c.getMethods();

            ArrayList<ServiceDescription> methodList = new ArrayList<ServiceDescription>();

            for (int i=0;i<methods.length;i++) {
                Method method = methods[i];
                if (method.getDeclaringClass().getName().equals(this.getClass().getName())) {  //only add the methods declared in the class that extends Invokable !!
                    ServiceDescription serviceDescription = new ServiceDescription();
                    serviceDescription.setMethodName(method.getDeclaringClass().getName()+"."+ method.getName());
                    serviceDescription.setReturnType(method.getReturnType());
                    serviceDescription.setParameterTypes(method.getParameterTypes());

                    methodList.add(serviceDescription);
                }
            }

            ServiceDiscoveryBroker.getInstance().addServiceDiscoveryResponse(serviceRequest.getSerialNumber(), methodList);




    }

    private void processPingMessage(InvocationMessage incomingMessage) {

        try {
            Method method = getMethod(incomingMessage.getMethodName(), incomingMessage.getParameters().toArray());

            if (method!=null) { //method found

                InvocationMessage pongMessage = new InvocationMessage();
                pongMessage.setType(InvocationMessage.PONG);
                pongMessage.setSerialNumber(incomingMessage.getSerialNumber());

                UdpUtils.sendMessage(pongMessage, encryptResponse, password, transportType, unicastAddress, port);
                System.out.println("pong-message sent");

            }

        } catch (Exception e) {
            
        }


    }


    private void processInvocationMessage(InvocationMessage invocationMessage) throws IOException {

        Object result = invoke(invocationMessage.getMethodName(), invocationMessage.getParameters().toArray());

        if (result!=null && result.getClass()== ClassNotTargetForInvocationException.class)
            return; //This class was not the intended target for the invocation message, send no response!


        
        sendResponse(invocationMessage, result);

    }

    private void sendResponse( InvocationMessage invocationMessage, Object result) throws IOException {     
        InvocationMessage responseMessage = new InvocationMessage();
        responseMessage.setSerialNumber(invocationMessage.getSerialNumber());
        responseMessage.setType(InvocationMessage.RESPONSE);
        responseMessage.setResponse(result);

        UdpUtils.sendMessage(responseMessage, encryptResponse, password, transportType, unicastAddress, port);
    }



    public Object invoke(String classAndMethodName, Object... object) {


        try {
            Method foundMethod = getMethod(classAndMethodName, object);
           


            return foundMethod.invoke(this, object);




        } catch (Exception e) {
            //e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
            return e;
        }


    }

    private Method getMethod(String classAndMethodName, Object... object) throws ClassNotTargetForInvocationException {
        Class c = this.getClass();


        String methodName = getMethodName(classAndMethodName);
        String className = getClassName(classAndMethodName);

        //check class
        if (className!=null) {
            if (!c.getName().equals(className) && !c.getSimpleName().equals(className))
                throw new ClassNotTargetForInvocationException(); //not correct class
        }


        Class[] parameterClasses = getParameterClasses(object);

        if (parameterClasses.length==0) {
            parameterClasses=null; //no parameters
        }

        Method foundMethod =null;

        try {
            foundMethod = c.getMethod(methodName, parameterClasses);
        } catch (NoSuchMethodException e) {
            throw new ClassNotTargetForInvocationException(e);
        }
        return foundMethod;
    }

    private String getClassName(String classAndMethodName) {

        if (!classAndMethodName.contains("."))
            return null;
        else {
            int lastDotPos = classAndMethodName.lastIndexOf(".");
            return classAndMethodName.substring(0, lastDotPos);

        }

    }

    private String getMethodName(String classAndMethodName) {

        if (!classAndMethodName.contains("."))
            return classAndMethodName;
        else {
            int lastDotPos = classAndMethodName.lastIndexOf(".");
            return classAndMethodName.substring(lastDotPos+1);
        }

    }

    private Class[] getParameterClasses(Object[] object) {

        Class[] classes = new Class[object.length];

        int i=0;
        for (Object o : object) {
            classes[i] =o.getClass();

            i++;

        }

        return classes;

    }


    public void terminate() {
        stop = true;
    }

    public void addMessageToQue(byte[] buffer) {
        try {
            messagesQueue.put(buffer);
            //System.out.println("buffer put on Invokable's queue");
        } catch (InterruptedException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        }
    }

   
}
